﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Configuration;
using System.DirectoryServices.AccountManagement;
using System.Linq;
using System.Net.Mail;
using System.Reflection;
using System.Text;
using System.Web;
using Microsoft.SharePoint;
using Microsoft.SharePoint.Administration;

namespace SP.FUPS
{
    class Program
    {
        private static bool _update = false;
        private static LogFile _FUPSLog = null;
        private static string _FUPSLogFileName = string.Empty;
        private static string _SmtpServer = string.Empty;
        private static int _ctrErrorsLogged = 0;
        private static int _ctrErrorsEmailed = 0;
        private static int _ctrTotal = 0;
        private static int _ctrUpdated = 0;
        private static int _ctrRemoved = 0;
        private static int _ctrInactiveInAD = 0;
        private static int _ctrNotInAD = 0;
        private static int _ctrRemoveAllDomain = 0;
        private static UserPrincipalExt _userAD = null;

        private static int _maxErrors = 10; // limits error emails and logging to this number.  Used with _ctrError
        private static int _maxStringLength = 55;

        // UIL field names
        private static string _emailFieldName = string.Empty;
        private static string _titleFieldName = string.Empty;
        private static string _departmentFieldName = string.Empty;
        private static string _jobtitleFieldName = string.Empty;

        // log file verbiage
        private static string _updateStringPlural = string.Empty;
        private static string _updateStringSingular = string.Empty;
        private static string _removeStringPlural = string.Empty;
        private static string _removeStringSingular = string.Empty;
        private static string _removeAllDomainString = string.Empty;

        // from app.config file
        private static string _FUPSLogDirectory = string.Empty;
        private static string _domain = string.Empty;
        private static string _emailFromAddress = string.Empty;
        private static string _emailToAddresses = string.Empty;
        private static string _excludeString = string.Empty;
        private static string _webAppString = string.Empty;
        private static List<string> _excludeList = new List<string>();
        private static List<string> _webAppList = new List<string>();
        private static bool _remove = false;
        private static string _removeAllDomain = string.Empty;

        /// <summary>
        /// Enumerates types of log entries available.
        /// </summary>
        private enum logType
        {
            INFO,
            REMOVE,
            UPDATE,
            DEBUG,
            ERROR,
            SETTING
        }

        static void Main(string[] args)
        {
            try
            {
                bool argsOK = false;
                string msg = string.Empty;

                // check number of arguments present
                if (args.Length != 1)
                {
                    msg = string.Format("There should be one argument.  You entered {0} arguments.  {1}See below for correct syntax.",
                        args.Length, System.Environment.NewLine);
                    outputHelp(msg);
                }
                else if (args[0].ToUpper() != @"/UPDATE" && args[0].ToUpper() != @"/NOUPDATE" && args[0] != "/?") // check for correct argument
                {
                    msg = string.Format("The argument you entered [{0}] is not one of the options.  {1}See below for correct syntax.",
                        args[0], System.Environment.NewLine);
                    outputHelp(msg);
                }
                else // correct argument
                {
                    argsOK = true;
                }

                if (argsOK)
                {
                    SPSecurity.CodeToRunElevated elevatedFUPS = new SPSecurity.CodeToRunElevated(FUPS);

                    switch (args[0].ToUpper())
                    {
                        case @"/?":
                            outputHelp("");
                            break;
                        case @"/UPDATE":
                            // set update flag
                            _update = true;
                            _updateStringPlural = "were updated";
                            _updateStringSingular = "was updated";

                            SPSecurity.RunWithElevatedPrivileges(elevatedFUPS);
                            break;
                        case @"/NOUPDATE":
                            // set update flag
                            _update = false;
                            _updateStringPlural = "would be updated";
                            _updateStringSingular = "would be updated";

                            SPSecurity.RunWithElevatedPrivileges(elevatedFUPS);
                            break;
                    }

                }

            }
            catch (Exception ex)
            {
                Console.WriteLine(FormatedExceptionLog(ex));
                //Console.Read();
                return;
            }
        }

        private static void outputHelp(string msg)
        {
            Console.WriteLine(msg + System.Environment.NewLine);
            Console.WriteLine("USAGE:");
            Console.WriteLine("   FoundationUPS [/? | /Update | /NoUpdate]" + System.Environment.NewLine);
            Console.WriteLine("   Options:");
            Console.WriteLine("      /?         Displays this help message.");
            Console.WriteLine("      /Update    Updates/removes items based on AD information.");
            Console.WriteLine("      /NoUpdate  Writes to log but makes no changes.");
            //Console.WriteLine("press any key ...");
            //Console.Read();
        }

        public static void FUPS()
        {

            try
            {
                Console.Clear();
                Console.WriteLine("Application started...");

                // get settings from app.config file
                getConfigSettings();

                // setup logging
                setupLog();

                // log initial settings
                logInitialSettings();

                // get reference to the local farm
                SPServiceCollection services = SPFarm.Local.Services;

                // go through each service and check if it's a web service
                foreach (SPService service in services.Where(s => s is SPWebService))
                {
                    // get reference to the web service
                    SPWebService webService = (SPWebService)service;

                    // go through each web application of the web service (if it's in the list)
                    foreach (SPWebApplication webApp in webService.WebApplications.Where(w => _webAppList.Contains(w.Name)))
                    {
                        _FUPSLog.createLogEntry(logType.INFO.ToString(), "Web App: " + webApp.Name);

                        // get a reference to the site collections
                        SPSiteCollection siteCollections = webApp.Sites;

                        // go through each site collection
                        foreach (SPSite siteCollection in siteCollections)
                        {
                            _FUPSLog.createLogEntry(logType.INFO.ToString(), "=====================================================");
                            _FUPSLog.createLogEntry(logType.INFO.ToString(), "Site Collection: " + siteCollection.Url);
                            _FUPSLog.createLogEntry(logType.INFO.ToString(), "=====================================================");

                            //get SMTP server from SharePoint setup
                            _SmtpServer = siteCollection.WebApplication.OutboundMailServiceInstance.Server.Address.Trim();

                            //_FUPSLog.createLogEntry(logType.INFO.ToString(), "SMTP Server: " + _SmtpServer);

                            // get reference to the root web user information list (hidden user list)
                            SPList userInformationList = siteCollection.RootWeb.SiteUserInfoList;

                            // check that all UIL fields exist before processing site collection
                            if (!uilFieldsVerified(userInformationList))
                            {
                                _FUPSLog.createLogEntry(logType.INFO.ToString(),
                                    "One or more UIL fields were not found in this UIL.  Skipping this site collection.");
                                continue;
                            }

                            // get reference to the web
                            SPWeb web = siteCollection.OpenWeb();

                            // go through each item in the user info list
                            foreach (SPListItem item in userInformationList.Items)
                            {
                                // increment total counter
                                _ctrTotal++;

                                // set the cursor so overwrites previous value
                                Console.SetCursorPosition(0, 1);
                                Console.Write("Account: " + _ctrTotal);

                                string userAccount = item["Account"].ToString();
                                string isItemToProcessReturn = isItemToProcess(item);

                                // if item is part of remove all domain then remove
                                if (isItemToProcessReturn == "RemoveAll")
                                {
                                    // increment counter
                                    _ctrRemoveAllDomain++;
                                    removeAccount(web, item, userAccount);

                                    _FUPSLog.createLogEntry(logType.INFO.ToString(),
                                        "-----------------------------------------------------");
                                }

                                // if item is to be processed
                                else if (isItemToProcessReturn == "true")
                                {
                                    // check if searching AD for user returned error
                                    string userActiveInADReturn = userActiveInAD(userAccount);
                                    if (userActiveInADReturn != "skipOnError")
                                    {
                                        // if account exists in AD and is enabled, compare and update values as needed
                                        if (userActiveInADReturn == "true")
                                        {
                                            updateUserData(item, userAccount);
                                        }
                                        else // if doesn't exist in AD then process deletion based on flags
                                        {
                                            if (_remove) removeAccount(web, item, userAccount);

                                            _FUPSLog.createLogEntry(logType.INFO.ToString(),
                                                "-----------------------------------------------------");

                                        }
                                    }// if (userActiveInADReturn != "skipOnError")
                                }// if (isItemToProcess(item))
                            }// foreach(SPListItem item in userInformationList.Items)

                        }// foreach(SPSite siteCollection in siteCollections)
                    }// foreach(SPWebApplication webApp in webService.WebApplications)

                }// foreach(SPService service in services)

                logResults();

            }
            catch (LogLibraryException ex)
            {
                // if smtp server is set send email
                if (_SmtpServer.Length > 0) sendErrorEmail(ex);
                else Console.WriteLine("Error: " + ex.Message);
            }

            catch (Exception ex)
            {
                //add to log
                logError(ex);

                // if smtp server is set send email
                if (_SmtpServer.Length > 0) sendErrorEmail(ex);

                throw;
            }
        }

        /// <summary>
        /// Verifies and sets UIL field names to match site collection setup
        /// </summary>
        /// <returns>true if all the fields are found in the UIL.  false otherwise</returns>
        private static bool uilFieldsVerified(SPList uil)
        {
            bool result = true;

            // determine if email field exists
            if (uil.Fields.ContainsField("EMail")) _emailFieldName = "EMail";
            else result = false;

            // determine if title field exists
            if (uil.Fields.ContainsField("Title")) _titleFieldName = "Title";
            else result = false;

            // determine if department field exists
            if (uil.Fields.ContainsField("Department")) _departmentFieldName = "Department";
            else result = false;

            // determine if job title field exists
            if (uil.Fields.ContainsField("JobTitle")) _jobtitleFieldName = "JobTitle";
            else result = false;

            return result;
        }

        /// <summary>
        /// Removes account from UIL if update flag is set.
        /// </summary>
        /// <param name="web">current web</param>
        /// <param name="item">current list item (account)</param>
        /// <param name="userAccount">user account in format domain\username</param>
        private static void removeAccount(SPWeb web, SPListItem item, string userAccount)
        {

            // increment removed counter
            _ctrRemoved++;

            if (_update)
            {
                web.AllowUnsafeUpdates = true;
                web.SiteUsers.RemoveByID(item.ID);
                web.Update();
                web.AllowUnsafeUpdates = false;
            }

            _FUPSLog.createLogEntry(logType.REMOVE.ToString(),
                string.Format("[{0}] {1} from list.", userAccount, _removeStringSingular));

        }

        /// <summary>
        /// Sets up logging
        /// </summary>
        public static void setupLog()
        {
            try
            {
                // get assembly version
                string assemblyVersion = Assembly.GetExecutingAssembly().GetName().Version.ToString();

                // create log file
                _FUPSLog = new LogFile("FUPS_", _FUPSLogDirectory);

                // get fully qualified filename
                _FUPSLogFileName = _FUPSLog.logDir + "\\" + _FUPSLog.logFileName;
                _FUPSLog.createLogEntry(logType.INFO.ToString(), "Version: " + assemblyVersion);
                _FUPSLog.createLogEntry(logType.INFO.ToString(), "Log file for FUPS");

            }
            catch (LogLibraryException ex)
            {
                ex.Data.Add("Program.setupLog.FUPSLogDirectory", _FUPSLogDirectory);
                throw;
            }

        }

        /// <summary>
        /// Get settings from app.config file
        /// </summary>
        private static void getConfigSettings()
        {
            try
            {
                // get log directory
                if (ConfigurationManager.AppSettings.AllKeys.Contains("FUPSLogDirectory"))
                {
                    _FUPSLogDirectory = ConfigurationManager.AppSettings["FUPSLogDirectory"];
                }
                else
                {
                    throw new ConfigurationErrorsException(string.Format("Could not find key [{0}] in app.config file.",
                        "FUPSLogDirectory"));
                }

                // get domain
                if (ConfigurationManager.AppSettings.AllKeys.Contains("Domain"))
                {
                    _domain = ConfigurationManager.AppSettings["Domain"];
                }
                else
                {
                    throw new ConfigurationErrorsException(string.Format("Could not find key [{0}] in app.config file.",
                        "Domain"));
                }

                // get email from address
                if (ConfigurationManager.AppSettings.AllKeys.Contains("EmailFromAddress"))
                {
                    _emailFromAddress = ConfigurationManager.AppSettings["EmailFromAddress"];
                }
                else
                {
                    throw new ConfigurationErrorsException(string.Format("Could not find key [{0}] in app.config file.",
                        "EmailFromAddress"));
                }

                // get list of email addresses that will receive emails
                if (ConfigurationManager.AppSettings.AllKeys.Contains("EmailToAddresses"))
                {
                    _emailToAddresses = ConfigurationManager.AppSettings["EmailToAddresses"];
                }
                else
                {
                    throw new ConfigurationErrorsException(string.Format("Could not find key [{0}] in app.config file.",
                        "EmailToAddresses"));
                }

                // get list of usernames to exclude
                if (ConfigurationManager.AppSettings.AllKeys.Contains("ExcludeList"))
                {
                    _excludeString = ConfigurationManager.AppSettings["ExcludeList"];

                    // split into list
                    _excludeList = new List<string>(_excludeString.Split(','));

                }
                else
                {
                    throw new ConfigurationErrorsException(string.Format("Could not find key [{0}] in app.config file.",
                        "ExcludeList"));
                }


                // get list of web apps to process
                if (ConfigurationManager.AppSettings.AllKeys.Contains("WebAppList"))
                {
                    _webAppString = ConfigurationManager.AppSettings["WebAppList"];

                    // split into list
                    _webAppList = new List<string>(_webAppString.Split(','));

                }
                else
                {
                    throw new ConfigurationErrorsException(string.Format("Could not find key [{0}] in app.config file.",
                        "WebAppList"));
                }

                // get remove flag
                if (ConfigurationManager.AppSettings.AllKeys.Contains("Remove"))
                {
                    string removeFlag = ConfigurationManager.AppSettings["Remove"];

                    if (removeFlag.ToUpper() == "TRUE") _remove = true;
                    else _remove = false;

                    // set remove wording
                    if (_update)
                    {
                        _removeStringPlural = "were removed";
                        _removeStringSingular = "was removed";
                    }
                    else
                    {
                        _removeStringPlural = "would be removed";
                        _removeStringSingular = "would be removed";
                    }

                }
                else
                {
                    throw new ConfigurationErrorsException(string.Format("Could not find key [{0}] in app.config file.",
                        "Remove"));
                }

                // get remove all domain
                if (ConfigurationManager.AppSettings.AllKeys.Contains("RemoveAllDomain"))
                {
                    _removeAllDomain = ConfigurationManager.AppSettings["RemoveAllDomain"];
                }
                else
                {
                    throw new ConfigurationErrorsException(string.Format("Could not find key [{0}] in app.config file.",
                        "RemoveAllDomain"));
                }


            }
            catch (ConfigurationErrorsException ex)
            {
                throw;
            }
        }

        /// <summary>
        /// Logs args and settings from app.config
        /// </summary>
        private static void logInitialSettings()
        {
            _FUPSLog.createLogEntry(logType.SETTING.ToString(), @"*** Settings ***");


            if (_update)
            {
                _FUPSLog.createLogEntry(logType.SETTING.ToString(),
                    @"* [/Update] argument used.");
            }
            else
            {
                _FUPSLog.createLogEntry(logType.SETTING.ToString(),
                    @"* [/NoUpdate] argument used.");
            }

            _FUPSLog.createLogEntry(logType.SETTING.ToString(), "FUPSLogDirectory: " + _FUPSLogDirectory);
            _FUPSLog.createLogEntry(logType.SETTING.ToString(), "Domain: " + _domain);
            _FUPSLog.createLogEntry(logType.SETTING.ToString(), "EmailFromAddress: " + _emailFromAddress);
            _FUPSLog.createLogEntry(logType.SETTING.ToString(), "EmailToAddresses: " + _emailToAddresses);
            _FUPSLog.createLogEntry(logType.SETTING.ToString(), "ExcludeList: " + _excludeString);
            _FUPSLog.createLogEntry(logType.SETTING.ToString(), "WebAppList: " + _webAppString);
            _FUPSLog.createLogEntry(logType.SETTING.ToString(), "Remove: " + _remove);
            _FUPSLog.createLogEntry(logType.SETTING.ToString(), "RemoveAllDomain: " + _removeAllDomain);
            _FUPSLog.createLogEntry(logType.SETTING.ToString(), @"*** End Settings ***");


        }

        /// <summary>
        /// Logs results from processing (totals, flag settings)
        /// </summary>
        private static void logResults()
        {
            // log results
            _FUPSLog.createLogEntry(logType.INFO.ToString(),
                string.Format("{0} accounts processed", _ctrTotal));
            _FUPSLog.createLogEntry(logType.INFO.ToString(),
                string.Format("{0} accounts {1}", _ctrUpdated, _updateStringPlural));
            _FUPSLog.createLogEntry(logType.INFO.ToString(),
                string.Format("{0} accounts not in AD", _ctrNotInAD));
            _FUPSLog.createLogEntry(logType.INFO.ToString(),
                string.Format("{0} accounts in AD but inactive", _ctrInactiveInAD));
            if (_remove) _FUPSLog.createLogEntry(logType.INFO.ToString(),
               string.Format("{0} accounts {1}", _ctrRemoved, _removeStringPlural));

            if (_removeAllDomain.Length > 0)
            {
                string removeAllString = "would be";
                if (_update) removeAllString = "were";

                _FUPSLog.createLogEntry(logType.INFO.ToString(),
                    string.Format("{0} accounts from the [{1}] domain {2} removed.",
                    _ctrRemoveAllDomain, _removeAllDomain, removeAllString));
            }


            Console.WriteLine();
            Console.WriteLine("Processing Complete.");
            Console.WriteLine();
            Console.WriteLine(string.Format("See {0} for results.", _FUPSLogFileName));
        }


        /// <summary>
        /// Checks to see if item should be processed.  If item is part of current domain, is of type Person,
        /// is not a site admin, is not in the exclusion list and is not a service account
        /// then returns true.
        /// </summary>
        /// <param name="item">User Information List entry</param>
        /// <returns>True if item should be processed, "RemoveAll" if item is part of remove all domain.</returns>
        public static string isItemToProcess(SPListItem item)
        {
            string result = "false";

            // check if of type "person" and not an admin
            string type = item["Content Type"].ToString();
            bool isSiteAdmin = Convert.ToBoolean(item["Is Site Admin"]);

            if (type == "Person" && !isSiteAdmin)
            {
                // get username and domain
                string userAccount = item["Account"].ToString();
                int slashIndex = userAccount.LastIndexOf("\\");
                string userName = userAccount.Substring(slashIndex + 1);
                string domain = userAccount.Substring(0, slashIndex);

                // don't process if account is not part of current domain
                if (domain.ToUpper() == _domain.ToUpper())
                {
                    // check if present in exclusion list or starts with "svc_"
                    if (_excludeList.Contains(userName) || userName.ToUpper().StartsWith("SVC_")) result = "false";
                    else result = "true";
                }
                // if domain is marked for complete removal
                else if (domain.ToUpper() == _removeAllDomain.ToUpper()) result = "RemoveAll";
            }
            return result;
        }

        /// <summary>
        /// Checks if user is in AD and active.
        /// </summary>
        /// <param name="userAccount">User account in the format domain\username</param>
        /// <returns>"true" if user active in AD, "false" in inactive, "skipOnError" if error occurs</returns>
        public static string userActiveInAD(string userAccount)
        {
            string result = string.Empty;

            // get username and domain
            int slashIndex = userAccount.LastIndexOf("\\");
            string userName = userAccount.Substring(slashIndex + 1);
            string domain = userAccount.Substring(0, slashIndex);

            try
            {
                // set context to current domain
                PrincipalContext context = new PrincipalContext(ContextType.Domain, _domain + ".net");

                using (context)
                {
                    // get principal for this user
                    _userAD = UserPrincipalExt.FindByIdentity(
                        context,
                        System.DirectoryServices.AccountManagement.IdentityType.SamAccountName,
                        userName);

                    // if user exists
                    if (_userAD != null)
                    {

                        // check if user is enabled
                        if (_userAD.Enabled == true)
                        {
                            //_FUPSLog.createLogEntry(logType.DEBUG.ToString(), "User in AD and enabled.");


                            result = "true";
                        }
                        else // user in AD but inactive
                        {
                            _FUPSLog.createLogEntry(logType.INFO.ToString(),
                                string.Format("User [{0}] is inactive in AD.", userAccount));

                            // increment counter
                            _ctrInactiveInAD++;

                            result = "false";
                        }
                    }
                    else // user is not in AD
                    {
                        _FUPSLog.createLogEntry(logType.INFO.ToString(),
                            string.Format("User [{0}] is not in AD.", userAccount));

                        // increment counter
                        _ctrNotInAD++;

                        result = "false";
                    }

                    return result;
                }

            }
            catch (Exception ex)
            {
                ex.Data.Add("userActiveInAD.userAccount", userAccount);

                //_FUPSLog.createLogEntry(logType.ERROR.ToString(),
                //    string.Format("Search for user account [{0}] in AD caused a {1}. This account was not processed.",
                //    userAccount, ex.GetType().ToString()));
                //_FUPSLog.createLogEntry(logType.INFO.ToString(),
                //    "-----------------------------------------------------");

                logError(ex);

                sendErrorEmail(ex);

                return "skipOnError";
            }


        }

        /// <summary>
        /// Updates user account information in UIL w/ AD (if info is different)
        /// </summary>
        /// <param name="item">UIL lit item</param>
        /// <param name="userAccount">UIL users login</param>
        private static void updateUserData(SPListItem item, string userAccount)
        {
            try
            {
                string itemEmail = string.Empty;
                string itemTitle = string.Empty;
                string itemDepartment = string.Empty;
                string itemJobTitle = string.Empty;

                string userADEmail = string.Empty;
                string userADDisplayName = string.Empty;
                string userADDepartment = string.Empty;
                string userADDescription = string.Empty;

                bool itemModified = false;

                itemEmail = item[_emailFieldName] == null ? "" : item[_emailFieldName].ToString();
                itemTitle = item[_titleFieldName] == null ? "" : item[_titleFieldName].ToString();
                itemDepartment = item[_departmentFieldName] == null ? "" : item[_departmentFieldName].ToString();
                itemJobTitle = item[_jobtitleFieldName] == null ? "" : item[_jobtitleFieldName].ToString();

                userADEmail = validatedString(_userAD.EmailAddress);
                userADDisplayName = validatedString(_userAD.DisplayName);
                userADDepartment = validatedString(_userAD.Department);
                userADDescription = validatedString(_userAD.Description);

                // compare item and userAD values and update if different
                if (itemEmail != userADEmail)
                {
                    // update item if /Update flag is set
                    if (_update) item[_emailFieldName] = userADEmail;

                    _FUPSLog.createLogEntry(logType.UPDATE.ToString(),
                        string.Format("User [{0}] EMAIL {1} from [{2}] to [{3}].", userAccount, _updateStringSingular, itemEmail, userADEmail));

                    itemModified = true;
                }

                if (itemTitle != userADDisplayName)
                {
                    // update item if /Update flag is set
                    if (_update) item[_titleFieldName] = userADDisplayName;

                    // log if data was truncated (i.e. last 3 characters are ellipsis
                    if (userADDisplayName.Length > 3)
                    {
                        if (userADDisplayName.Substring(userADDisplayName.Length - 3) == "...")
                            _FUPSLog.createLogEntry(logType.UPDATE.ToString(),
                                string.Format("Display name was truncated to [{0}]", userADDisplayName));
                    }

                    _FUPSLog.createLogEntry(logType.UPDATE.ToString(),
                        string.Format("User [{0}] DISPLAY NAME {1} from [{2}] to [{3}].", userAccount, _updateStringSingular, itemTitle, userADDisplayName));

                    itemModified = true;
                }

                if (itemDepartment != userADDepartment)
                {
                    // update item if /Update flag is set
                    if (_update) item[_departmentFieldName] = userADDepartment;

                    // log if data was truncated (i.e. last 3 characters are ellipsis
                    if (userADDepartment.Length > 3)
                    {
                        if (userADDepartment.Substring(userADDepartment.Length - 3) == "...")
                            _FUPSLog.createLogEntry(logType.UPDATE.ToString(),
                                string.Format("Department was truncated to [{0}]", userADDepartment));
                    }

                    _FUPSLog.createLogEntry(logType.UPDATE.ToString(),
                        string.Format("User [{0}] DEPARTMENT {1} from [{2}] to [{3}].", userAccount, _updateStringSingular, itemDepartment, userADDepartment));

                    itemModified = true;
                }

                if (itemJobTitle != userADDescription)
                {
                    // update item if /Update flag is set
                    if (_update) item[_jobtitleFieldName] = userADDescription;

                    // log if data was truncated (i.e. last 3 characters are ellipsis
                    if (userADDescription.Length > 3)
                    {
                        if (userADDescription.Substring(userADDescription.Length - 3) == "...")
                            _FUPSLog.createLogEntry(logType.UPDATE.ToString(),
                                string.Format("Description was truncated to [{0}]", userADDescription));
                    }

                    _FUPSLog.createLogEntry(logType.UPDATE.ToString(),
                        string.Format("User [{0}] JOB TITLE {1} from [{2}] to [{3}].", userAccount, _updateStringSingular, itemJobTitle, userADDescription));

                    itemModified = true;
                }

                // save changes if /Update flag is set
                if (_update) item.Update();

                // increment counter if any changes were made
                if (itemModified)
                {
                    _FUPSLog.createLogEntry(logType.INFO.ToString(), "---------------------------------------");

                    _ctrUpdated++;
                }
            }
            catch (Exception ex)
            {
                ex.Data.Add("updateUserData.userAccount", userAccount);
                ex.Data.Add("updateUserData.email", _userAD.EmailAddress);
                ex.Data.Add("updateUserData.displayName", _userAD.DisplayName);
                ex.Data.Add("updateUserData.department", _userAD.Department);
                ex.Data.Add("updateUserData.description", _userAD.Description);

                //add to log
                logError(ex);

                // if smtp server is set send email
                if (_SmtpServer.Length > 0) sendErrorEmail(ex);
            }

        }

        /// <summary>
        /// Validates an input string
        /// </summary>
        /// <param name="input"></param>
        /// <returns>Empty string if input is null or if input length is > max string length, 
        ///     returns truncated string with ellipsis</returns>
        private static string validatedString(string input)
        {
            string output = input;

            // return empty string if null.  If > 50 characters return first 47 + ellipsis
            if (input == null) output = string.Empty;
            else if (input.Length > _maxStringLength)
            {
                output = input.Substring(0, _maxStringLength - 3) + "...";

            }

            return output;
        }

        /// <summary>
        /// Takes object of type Exception and formats for logging.
        /// </summary>
        /// <param name="inner">Object of type Exception.</param>
        /// <returns>Formated string for use in logging exception.</returns>
        private static string FormatedExceptionLog(Exception inner)
        {
            string exceptionFormated = System.Environment.NewLine + "Error details: " + System.Environment.NewLine +
                "TimeStamp: " + DateTime.Now + System.Environment.NewLine +
                "Type:" + inner.GetType().ToString() + System.Environment.NewLine +
                "Description: " + inner.Message + System.Environment.NewLine +
                "Source: " + inner.Source + System.Environment.NewLine +
                "Class defining member: " + inner.TargetSite.DeclaringType + System.Environment.NewLine +
                "Member type: " + inner.TargetSite.MemberType + System.Environment.NewLine +
                "Stack: " + inner.StackTrace;

            // Check for additional exception data and add to msg
            if (inner.Data.Count > 0)
            {
                string dataEntries = string.Empty;
                foreach (DictionaryEntry de in inner.Data)
                {
                    dataEntries += de.Key + ": " + de.Value + System.Environment.NewLine;
                }
                exceptionFormated += System.Environment.NewLine + "Exception data:"
                    + System.Environment.NewLine + dataEntries;
            }

            return exceptionFormated;
        }


        private static void logError(Exception ex)
        {
            if (_ctrErrorsLogged < _maxErrors) _FUPSLog.createLogEntry(logType.ERROR.ToString(), FormatedExceptionLog(ex));
            else if (_ctrErrorsLogged == _maxErrors) _FUPSLog.createLogEntry(logType.ERROR.ToString(),
                string.Format("Maximum [{0}] logged errors reached.  Suspending logging of errors", _maxErrors));

            // increment error counter
            _ctrErrorsLogged++;
        }

        /// <summary>
        /// Send email to admin group if errors occur.
        /// </summary>
        /// <param name="ex">Exception from Catch</param>
        public static void sendErrorEmail(Exception ex)
        {
            // if exceeded maximum number of error emails then exit
            if (_ctrErrorsEmailed > _maxErrors) return;

            string msgSubject = string.Empty;
            string msgBody = string.Empty;
            string msgFrom = string.Empty;
            string msgTo = string.Empty;

            msgFrom = _emailFromAddress;
            msgTo = _emailToAddresses;

            msgSubject = "FUPS error";

            if (_ctrErrorsEmailed < _maxErrors)
            {
                msgBody = "<p><b> Error details: </b></p>" +
                    "<b>TimeStamp:</b> " + DateTime.Now + "<br>" +
                    "<b>Type:</b> " + ex.GetType().ToString() + "<br>" +
                    "<b>Description:</b> " + ex.Message + "<br>" +
                    "<b>Source:</b> " + ex.Source + "<br>" +
                    "<b>Class defining member:</b> " + ex.TargetSite.DeclaringType + "<br>" +
                    "<b>Member type:</b> " + ex.TargetSite.MemberType + "<br>" +
                    "<b>Stack:</b> " + ex.StackTrace;

                // Check for additional exception data and add to msg
                if (ex.Data.Count > 0)
                {
                    string dataEntries = string.Empty;
                    foreach (DictionaryEntry de in ex.Data)
                        dataEntries += de.Key + ": " + de.Value + "<br>";
                    msgBody += "<br> <b>Exception data:</b> <br>" + dataEntries;
                }
            }
            else msgBody = string.Format("Maximum [{0}] emailed errors reached.  Suspending emailing of errors", _maxErrors);

            MailMessage msg = new MailMessage(msgFrom, msgTo);

            // remove html encoding from subject
            msg.Subject = HttpUtility.HtmlDecode(msgSubject);
            msg.Body = msgBody;

            //set message to HTML format
            msg.IsBodyHtml = true;

            //get SMTP client
            SmtpClient client = new SmtpClient(_SmtpServer);

            //send email
            client.Send(msg);

            // increment error counter
            _ctrErrorsEmailed++;

        }

    }

    /// <summary>
    /// Extend UserPrincipal Class to add department property
    /// </summary>
    [DirectoryRdnPrefix("CN")]
    [DirectoryObjectClass("Person")]
    public class UserPrincipalExt : UserPrincipal
    {
        public UserPrincipalExt(PrincipalContext context) : base(context) { }

        public UserPrincipalExt(PrincipalContext context, string samAccountName, string password, Boolean enabled)
            : base(context, samAccountName, password, enabled)
        {
        }

        // create the department property
        [DirectoryProperty("department")]
        public string Department
        {
            get
            {
                if (ExtensionGet("department").Length != 1)
                    return null;

                return (string)ExtensionGet("department")[0];

            }
            set { this.ExtensionSet("department", value); }

        }

        // Implement the overloaded search method FindByIdentity.
        public static new UserPrincipalExt FindByIdentity(
            PrincipalContext context,
            System.DirectoryServices.AccountManagement.IdentityType identityType,
            string identityValue)
        {
            return (UserPrincipalExt)FindByIdentityWithType(context, typeof(UserPrincipalExt), identityType, identityValue);
        }
    }
}
